/*
 * @(#)AbstractQueryRequestTransformer.java
 * Date 2013-08-27
 * Version 1.0
 * Author Jim Horner
 * Copyright (c)2012
 */


package us.hornerscorners.lamppost.registry.transformer.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import javax.inject.Inject;

import oasis.names.tc.ebxml_regrep.xsd.query._3.AdhocQueryRequest;

import oasis.names.tc.ebxml_regrep.xsd.rim._3.AdhocQueryType;
import oasis.names.tc.ebxml_regrep.xsd.rim._3.SlotType1;
import oasis.names.tc.ebxml_regrep.xsd.rim._3.ValueListType;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import org.joda.time.DateTime;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import us.hornerscorners.lamppost.exception.ConversionException;
import us.hornerscorners.lamppost.exception.ServiceActionError;
import us.hornerscorners.lamppost.exception.ServiceActionException;
import us.hornerscorners.lamppost.exception.SeverityType;
import us.hornerscorners.lamppost.exception.ValidationException;

import us.hornerscorners.lamppost.registry.config.AdhocQueryConfig;
import us.hornerscorners.lamppost.registry.config.QueryMethodConfig;
import us.hornerscorners.lamppost.registry.config.QueryParameterConfig;

import us.hornerscorners.lamppost.registry.constant.AvailabilityStatus;
import us.hornerscorners.lamppost.registry.constant.ErrorCode;
import us.hornerscorners.lamppost.registry.constant.QuerySlotName;
import us.hornerscorners.lamppost.registry.constant.XDSDataType;

import us.hornerscorners.lamppost.registry.model.DataTypeValidator;
import us.hornerscorners.lamppost.registry.model.QueryParameter;
import us.hornerscorners.lamppost.registry.model.QueryRequest;
import us.hornerscorners.lamppost.registry.model.QueryReturnType;

import us.hornerscorners.lamppost.registry.utils.XDSUtils;

import us.hornerscorners.lamppost.transformer.AbstractTransformer;


/**
 *
 * @author jim
 */
public abstract class AbstractQueryRequestTransformer
        extends AbstractTransformer<AdhocQueryRequest, QueryRequest> {

    /** Field description */
    private final QuerySlotName[] ENTRY_STATUS_PARAMS;

    /** Field description */
    private final AvailabilityStatus[] ENTRY_STATUS_VALUES;

    /** Field description */
    private final QuerySlotName[][] FROM_TO_PAIRS;

    /** Field description */
    private final XDSDataType[] STRING_TYPES;

    /** Field description */
    private final Logger logger = LoggerFactory.getLogger(getClass());

    /** Field description */
    @Inject
    private DataTypeValidator validator;

    /**
     * Constructs ...
     *
     */
    public AbstractQueryRequestTransformer() {

        super();

        this.STRING_TYPES = new XDSDataType[] { XDSDataType.ce, XDSDataType.cx,
                XDSDataType.oid, XDSDataType.string, XDSDataType.uuid };

        this.ENTRY_STATUS_PARAMS = new QuerySlotName[] {
            QuerySlotName.XDSDocumentEntryStatus,
            QuerySlotName.XDSFolderStatus, QuerySlotName.XDSSubmissionSetStatus };

        this.ENTRY_STATUS_VALUES = new AvailabilityStatus[] {
            AvailabilityStatus.Submitted,
            AvailabilityStatus.Approved, AvailabilityStatus.Deprecated };

        this.FROM_TO_PAIRS = new QuerySlotName[][] {

            { QuerySlotName.XDSDocumentEntryCreationTimeFrom,
              QuerySlotName.XDSDocumentEntryCreationTimeTo },
            { QuerySlotName.XDSDocumentEntryServiceStartTimeFrom,
              QuerySlotName.XDSDocumentEntryServiceStartTimeTo },
            { QuerySlotName.XDSDocumentEntryServiceStopTimeFrom,
              QuerySlotName.XDSDocumentEntryServiceStopTimeTo },
            { QuerySlotName.XDSFolderLastUpdateTimeFrom,
              QuerySlotName.XDSFolderLastUpdateTimeTo },
            { QuerySlotName.XDSSubmissionSetSubmissionTimeFrom,
              QuerySlotName.XDSSubmissionSetSubmissionTimeTo }
        };
    }

    /**
     * Method description
     *
     *
     * @return
     */
    protected abstract AdhocQueryConfig getQueryConfig();

    /**
     * Method description
     *
     *
     * @param slot
     *
     * @return
     */
    private Collection<String> parseQueryMultiple(SlotType1 slot,
            QueryParameterConfig paramConfig, AvailabilityStatus[] validValues)
            throws ConversionException, ValidationException {

        List<String> result = new ArrayList<>();

        List<String> slotValues = new ArrayList<>();

        if (slot.getValueList() != null) {
            slotValues.addAll(slot.getValueList().getValue());
        }

        QuerySlotName slotName = paramConfig.getSlotName();

        for (String slotValue : slotValues) {

            validateParentheses(slotName, slotValue);

            slotValue = StringUtils.removeStart(slotValue, "(");

            slotValue = StringUtils.removeEnd(slotValue, ")");

            String[] values = StringUtils.splitPreserveAllTokens(slotValue, ",");

            if (ArrayUtils.isNotEmpty(values)) {

                Collection<String> parsedValues = parseValues(paramConfig,
                                                      Arrays.asList(values));

                if (validValues == null) {

                    result.addAll(parsedValues);

                } else {

                    for (String checkValue : parsedValues) {

                        AvailabilityStatus checkStatus =
                            AvailabilityStatus.toAvailabilityStatus(checkValue);

                        if (ArrayUtils.contains(validValues, checkStatus)) {

                            result.add(checkValue);
                        }
                    }
                }
            }
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param slot
     *
     * @return
     */
    private Collection<String> parseValueList(SlotType1 slot,
            QueryParameterConfig paramConfig)
            throws ValidationException {

        List<String> result = new ArrayList<>();

        ValueListType valueList = slot.getValueList();

        if (valueList != null) {

            result.addAll(parseValues(paramConfig, valueList.getValue()));
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param values
     *
     * @return
     */
    private Collection<String> parseValues(QueryParameterConfig paramConfig,
            Collection<String> values)
            throws ValidationException {

        List<String> result = new ArrayList<>();

        boolean isString = ArrayUtils.contains(STRING_TYPES, paramConfig.getDataType());
        QuerySlotName slotName = paramConfig.getSlotName();

        for (String value : values) {

            value = StringUtils.trimToEmpty(value);

            if (isString) {

                validateQuotes(slotName, value);

                value = StringUtils.removeStart(value, "'");
                value = StringUtils.removeEnd(value, "'");
                value = StringUtils.replace(value, "''", "'");
            }

            this.validator.validate(slotName, paramConfig.getDataType(), value);
            result.add(value);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     *
     * @param adhocQueryRequest
     * @return
     *
     */
    @Override
    public QueryRequest transform(AdhocQueryRequest adhocQueryRequest)
            throws ServiceActionException {

        QueryRequest result = null;

        AdhocQueryType adhocQuery = adhocQueryRequest.getAdhocQuery();

        String home = adhocQuery.getHome();
        String queryId = adhocQuery.getId();

        QueryMethodConfig methodConfig = getQueryConfig().findMethodByURN(queryId);

        if (methodConfig == null) {

            String msg = String.format("Unknown AdhocQuery type [%s].", queryId);

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSUnknownStoredQuery.getCode(), msg,
                                       SeverityType.Error);

            throw new ConversionException(getErrorLocation(), error);
        }

        QueryReturnType returnType =
            QueryReturnType.toQueryReturnType(
                adhocQueryRequest.getResponseOption().getReturnType());

        if (returnType == null) {

            String msg =
                String.format("[%s] is an unrecognized returnType.",
                              adhocQueryRequest.getResponseOption().getReturnType());

            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            throw new ValidationException(getErrorLocation(), error);
        }

        result = new QueryRequest(home, methodConfig, returnType);

        for (SlotType1 slot : adhocQuery.getSlot()) {

            QuerySlotName slotName = QuerySlotName.toQuerySlotName(slot.getName());

            QueryParameterConfig paramConfig = methodConfig.findParameterConfig(slotName);

            if (paramConfig == null) {

                String msg =
                    String.format("[%s] is an unrecognized parameter for Query %s.",
                                  slot.getName(), methodConfig.getQueryName().getName());

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(),
                                           msg, SeverityType.Error);

                throw new ConversionException(getErrorLocation(), error);
            }

            QueryParameter param = new QueryParameter(paramConfig);

            // validate AND/OR syntax
            if (result.containsParameter(param)
                    && (paramConfig.isAndOrSupported() == false)) {

                String msg =
                    String.format(
                        "Duplicate slot [%s] and slot does not allow and/or syntax.",
                        slotName);

                ServiceActionError error =
                    new ServiceActionError(ErrorCode.XDSStoredQueryParamNumber.getCode(),
                                           msg, SeverityType.Error);

                throw new ValidationException(getErrorLocation(), error);
            }

            // validate multiples
            if (paramConfig.isMultipleAllowed()) {

                AvailabilityStatus[] validValues = null;

                // validate code set
                // TODO use a terminology service
                if (ArrayUtils.contains(ENTRY_STATUS_PARAMS, paramConfig.getSlotName())) {

                    validValues = ENTRY_STATUS_VALUES;
                }

                param.getValues().addAll(parseQueryMultiple(slot, paramConfig,
                        validValues));

            } else {

                param.getValues().addAll(parseValueList(slot, paramConfig));

                if (param.getValues().size() > 1) {

                    String msg =
                        String.format("Slot [%s] can not contain multiple values.",
                                      slotName);

                    ServiceActionError error =
                        new ServiceActionError(
                            ErrorCode.XDSStoredQueryParamNumber.getCode(), msg,
                            SeverityType.Error);

                    throw new ValidationException(getErrorLocation(), error);
                }
            }

            result.addParameter(param);
        }

        // validate required
        validateRequired(methodConfig, result);

        // validate to / from
        validateToFrom(result);

        return result;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     *
     * @throws ValidationException
     */
    private void validateParentheses(QuerySlotName slotName, String value)
            throws ValidationException {

        if ((StringUtils.startsWith(value, "(") == false)
                || (StringUtils.endsWith(value, ")") == false)) {

            String msg =
                String.format(
                    "Slot [%s] value must start and end with parentheses: [%s].",
                    slotName, value);
            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            throw new ValidationException(getClass(), error);
        }
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param value
     *
     * @throws ValidationException
     */
    private void validateQuotes(QuerySlotName slotName, String value)
            throws ValidationException {

        if ((StringUtils.startsWith(value, "'") == false)
                || (StringUtils.endsWith(value, "'") == false)) {

            String msg =
                String.format(
                    "Slot [%s] value must start and end with single quote: [%s].",
                    slotName.getCode(), value);
            ServiceActionError error =
                new ServiceActionError(ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                                       SeverityType.Error);

            throw new ValidationException(getClass(), error);
        }
    }

    /**
     * Method description
     *
     *
     * @param methodConfig
     * @param result
     *
     */
    protected void validateRequired(QueryMethodConfig methodConfig, QueryRequest result)
            throws ValidationException {

        for (QueryParameterConfig paramConfig : methodConfig.getParameters()) {

            boolean hasValues = false;

            if (paramConfig.isRequired()) {

                QuerySlotName slotName = paramConfig.getSlotName();

                for (QueryParameter param : result.getParameters(slotName)) {

                    if (param.getValues().isEmpty() == false) {

                        hasValues = true;

                        break;
                    }
                }

                if (hasValues == false) {

                    String msg =
                        String.format(
                            "Slot [%s] is required and contains no valid values.",
                            slotName.getCode());

                    ServiceActionError error =
                        new ServiceActionError(
                            ErrorCode.XDSStoredQueryMissingParam.getCode(), msg,
                            SeverityType.Error);

                    throw new ValidationException(getErrorLocation(), error);
                }
            }
        }
    }

    /**
     * Method description
     *
     *
     * @param result
     */
    private void validateToFrom(QueryRequest result) throws ValidationException {

        for (QuerySlotName[] pair : FROM_TO_PAIRS) {

            String fromValue = result.getFirstParameterValue(pair[0]);
            String toValue = result.getFirstParameterValue(pair[1]);

            if (StringUtils.isNotBlank(fromValue) && StringUtils.isNotBlank(toValue)) {

                // validate from is before to
                DateTime fromDate = XDSUtils.parseDTM(fromValue);
                DateTime toDate = XDSUtils.parseDTM(toValue);

                if (fromDate.isAfter(toDate)) {

                    String msg =
                        String.format("Slot [%s] must be a date before slot [%s].",
                                      pair[0].getCode(), pair[1].getCode());

                    ServiceActionError error =
                        new ServiceActionError(
                            ErrorCode.XDSRegistryMetadataError.getCode(), msg,
                            SeverityType.Error);

                    throw new ValidationException(getErrorLocation(), error);
                }
            }
        }
    }
}
